WTForms 组件的介绍


1. WTForms 的介绍

  • WTForms 是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证(即: WTForms组件 和 Django中的 Form组件类似)

2. WTForms 的作用

  • 生成 HTML 标签

  • 对 form 表单进行验证

3. WTForms 的安装

pip3 install wtforms -i https://pypi.douban.com/simple  # 使用豆瓣的镜像

创建对应的表单所需的HTML


  • 通过 widgets 插件创建表单所需的HTML

  • 常用的表单字段模块:

    • simple
    • core
    • html5

  • input -> 普通输入框

    • 写法一

from wtforms import Form
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    name = simple.StringField(
        label='用户名'
    )

    • 写法二

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    name = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
    )

  • password -> 密码输入框

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    password = simple.StringField(
        label='密码',
        widget=widgets.PasswordInput()
    )

  • textarea -> 文本域

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    article = simple.StringField(
        label='文章',
        widget=widgets.TextArea(),
        render_kw={'id': 'article', 'cols': '50', 'rows': '30'}
    )

  • file -> 选择文件 -> 单文件上传

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    img = simple.StringField(
        label='单文件上传',
        widget=widgets.FileInput()
    )

  • file -> 选择文件 -> 多文件上传

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    imgs = simple.StringField(
        label='多文件上传',
        widget=widgets.FileInput(multiple=True)
    )

  • hidden -> 隐藏域

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    uids = simple.StringField(
        label='隐藏域',
        widget=widgets.HiddenInput()
    )

  • submit -> 提交按钮

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


# 创建表单类
class IndexForm(Form):
    submit_btn = simple.StringField(
        label='submit 提交按钮',
        widget=widgets.SubmitInput()
    )

  • radio -> 单选按钮

from wtforms import Form
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    gender = core.RadioField(
        label='性别',
        choices=[(1, '男'), (2, '女')],
        coerce=int  # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
    )

  • select -> 下拉选框

from wtforms import Form
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    city = core.SelectField(
        label='城市',
        choices=[
            ('bj', '北京'),
            ('sh', '上海'),
        ]
    )

  • select -> 多选的下拉选框

from wtforms import Form
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    hobby = core.SelectMultipleField(
        label='爱好',
        choices=[
            (1, '篮球'),
            (2, '足球')
        ],
        coerce=int  # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
    )

  • checkbox -> 复选框

from wtforms import Form
from wtforms import widgets
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    favor = core.SelectMultipleField(
        label='喜好',
        choices=[
            (1, '篮球'),
            (2, '足球'),
        ],
        widget=widgets.ListWidget(prefix_label=False),
        option_widget=widgets.CheckboxInput(),
        coerce=int  # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
    )

  • checkbox -> 单选框

    • 写法一

from wtforms import Form
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    protocol = core.BooleanField(
        label='是否同意协议?'
    )

    • 写法二

from wtforms import Form
from wtforms import widgets
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    protocol = core.BooleanField(
        label='是否同意协议?',
        widget=widgets.CheckboxInput()
    )

  • input -> html5的邮箱表单

from wtforms import Form
from wtforms import widgets
from wtforms.fields import html5


# 创建表单类
class IndexForm(Form):
    email = html5.EmailField(
        label='邮箱',
        widget=widgets.TextInput(input_type='email')
    )

选择性标签实时更新问题


  • 在使用选择性标签时,需要注意choices的值可以是从数据库中获取到的值,但是由于是静态字段,获取的值无法实时更新,需要重写构造方法从而实现choice实时更新

import pymysql
from wtforms import Form
from wtforms.fields import core


# 创建表单类
class IndexForm(Form):
    city = core.SelectField(
        label='城市',
        choices=[],
        coerce=int
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        conn = pymysql.connect(host='localhost', user='root', password='', database='school', charset='utf8')
        cursor = conn.cursor()
        sql = 'select * from city'
        cursor.execute(sql)
        result = cursor.fetchall()  # ((1, '广州'), (2, '深圳'), (3, '东莞'))
        cursor.close()
        conn.close()

self.city.choices = result # 从数据库中获取数据,然后更新静态属性中的数据

常用的字段参数


  • widget -> 插件

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


class IndexForm(Form):
    password = simple.StringField(
        widget=widgets.PasswordInput()
    )

  • label -> 用于生成Label标签或显示内容

from wtforms import Form
from wtforms.fields import simple


class IndexForm(Form):
    name = simple.StringField(
        label='用户名'
    )

  • render_kw -> 用于定义相关的id class 或 html 中的自定义属性

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple


class IndexForm(Form):
    password = simple.StringField(
        label='密码',
        widget=widgets.PasswordInput(),
        render_kw={'id': 'password', 'class': 'blue_input', 'placeholder': '请输入密码'}
    )

  • validators -> 验证规则

    • 不能为空

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple


class IndexForm(Form):
    username = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
validators=[  # 验证规则
            validators.DataRequired(message='用户不能为空')
        ]
    )

    • 长度验证

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple


class IndexForm(Form):
    username = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
validators=[  # 验证规则
            validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d,且小于%(max)d')
        ]
    )

    • 正则验证

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple


class IndexForm(Form):
    password = simple.StringField(
        label='密码',
        widget=widgets.PasswordInput(),
validators=[  # 验证规则
            validators.Regexp(
                regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
                message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符'
            )
        ]
    )

    • 判断两次密码是否一致的验证

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple


class IndexForm(Form):
    pwd = simple.StringField(
        label='密码',
        widget=widgets.PasswordInput(),
        validators=[  # 验证规则
            validators.DataRequired(message='密码不能为空'),
        ]
    )
    pwd_confirm = simple.PasswordField(
        label='重复密码',
        widget=widgets.PasswordInput(),
validators=[  # 验证规则
validators.DataRequired(message='重复密码不能为空'),
            validators.EqualTo('pwd', message="两次密码输入不一致")
        ]
    )

    • 邮箱格式验证

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import html5


class IndexForm(Form):
    email = html5.EmailField(
        label='邮箱',
        widget=widgets.TextInput(input_type='email'),
validators=[  # 验证规则
            validators.Email(message='邮箱格式错误')
        ]
    )

  • default -> 默认值

from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
from wtforms.fields import core


class IndexForm(Form):
# input 输入框
    username = simple.StringField(
        widget=widgets.TextInput(),
        default='默认值'
    )

# radio 单选框
    gender = core.RadioField(
        label='性别',
        choices=[
            (1, '男'),
            (2, '女'),
        ],
        default=2
    )

 # select 下拉框
    city = core.SelectField(
        label='城市',
        choices=[
            ('bj', '北京'),
            ('sh', '上海'),
        ],
        default='sh'
    )

# select 多选下拉框
    hobby = core.SelectMultipleField(
        label='爱好',
        choices=[
            (1, '篮球'),
            (2, '足球'),
        ],
        default=[1, 2]
    )

 # checkbox 复选框
    favor = core.SelectMultipleField(
        label='喜好',
        choices=[
            (1, '篮球'),
            (2, '足球'),
        ],
        widget=widgets.ListWidget(prefix_label=False),
        option_widget=widgets.CheckboxInput(),
        default=[1, 2]
    )

# checkbox 单选框
    protocol = core.BooleanField(
        label='是否同意协议?',
        widget=widgets.CheckboxInput(),
        default=True
    )

模板相关


1. 将Form类所生成的HTML发送到模板中

# app.py

from flask import Flask, render_template, request
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.fields import simple

app = Flask(__name__)


class RegisterForm(Form):
    username = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
        validators=[
            validators.DataRequired(message='用户名不能为空'),
            validators.Length(min=3, message='用户名长度不能小于%(min)d')
        ],
        render_kw={'placeholder': '请输入用户名'}
    )
    password = simple.PasswordField(
        label='密码',
        validators=[
            validators.DataRequired(message='密码不能为空')
        ],
        widget=widgets.PasswordInput(),
        render_kw={'placeholder': '请输入密码'}
    )


@app.route('/registered', methods=['GET', 'POST'])
def registered():
    form_obj = RegisterForm()  # 实例化一个表单对象
    if request.method == 'POST':
        form_obj = RegisterForm(formdata=request.form)  # 将post过来的数据再次传入Form类中,然后对数据进行验证,如果数据有误,所实例化出来的form_obj就会拿到该条数据有误的相关信息
        if form_obj.validate():  # 判断post过来的数据是否有误
# 获取验证成功的数据
            print(form_obj.username.data)  # Kevin
            print(form_obj.password.data)  # 123
 # 将通过验证的数据保存到数据库中
            return '注册成功'
        else:
            print(form_obj.errors)  # 存放着所有form字段错误信息,只有进行了验证 form_obj.validate() 才会有错误信息 -> {'username': ['用户名长度不能小于3'], 'password': ['密码不能为空']}
    return render_template('registered.html', form_obj=form_obj)


if __name__ == '__main__':
    app.run()

2. 自定义

<form method="post" novalidate>
    <div class="form-group">
<!-- 获取label的名称 -->
        <lable>{{ form_obj.username.label }}</lable>
<!-- 获取相关的表单标签 -->
        {{ form_obj.username }}
 <!-- 获取第一条验证过后的错误信息 -->
        <span class="help-block">{{ form_obj.username.errors.0 }}</span>
    </div>
    <div class="form-group">
 <!-- 获取label的名称 -->
        <lable>{{ form_obj.password.label }}</lable>
<!-- 获取相关的表单标签 -->
        {{ form_obj.password }}
<!-- 获取第一条验证过后的错误信息 -->
        <span class="help-block">{{ form_obj.password.errors.0 }}</span>
    </div>
    <input type="submit" value="提交">
</form>

3. 循环生成生成表单所需的HTML

<form method="post" novalidate>

<!-- 直接对返回的form对象进行循环 -->
    {% for field in form_obj %}
        <p>{{ field.label }}:{{ field }} {{ field.errors.0 }}</p>
    {% endfor %}

    <input type="submit" value="提交">
</form>

4. 获取错误信息的注意事项

  • 不要使用 form_obj.errors.字段名.序列号,因为当 form_obj.errors.字段名 没有该字段名的时候就会报错

  • 获取错误信息的正确方式: form_obj.字段名.errors.序列号

  • 错误示范

<!-- 获取所有字段的错误信息 -> {'password': ['密码不能为空'], 'username': ['用户名长度不能小于3']} -->
{{ form_obj.errors }} 

<!-- 当 form_obj.errors 下没有 username 的错误信息那么就会报错 -->
{{ form_obj.errors.username.0 }}

  • 正确示范

{{ form_obj.username.errors.0 }}

将对应的数据自动填充到表单中(即:修改页面)


  • 将对应数据自动填写到表单中(即: 修改页面)

  • data 参数接收一个字典(即: 查询数据后得到的字典)

# app.py

from flask import Flask, render_template
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.fields import simple
from wtforms.fields import core

app = Flask(__name__)


class UserInfoForm(Form):
    username = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
        validators=[
            validators.DataRequired(message='用户名不能为空'),
            validators.Length(min=3, message='用户名长度不能小于%(min)d')
        ],
        render_kw={'placeholder': '请输入用户名'}
    )
    gender = core.RadioField(
        label='性别',
        choices=[
            (1, '男'),
            (2, '女'),
        ]
    )
    city = core.SelectField(
        label='城市',
        choices=[
            ('bj', '北京'),
            ('sh', '上海'),
        ]
    )


@app.route('/edit_user_info', methods=['GET'])
def edit_user_info():
user_info_data = {
        'username': 'Kevin',
        'gender': 1,
        'city': 'sh'
    }
    form_obj = UserInfoForm(data=user_info_data)  # 将查询到的数据传递给 data 参数,从而实现将对应数据自动填写到表单中(即: 修改页面)
    return render_template('edit_user_info.html', form_obj=form_obj)


if __name__ == '__main__':
    app.run()

csrf_token


  • flask 默认没有 csrf_token,但是 wtform 提供了 csrf_token

  • 注意: wtform 所提供的 CSRF 的功能是不完整的,需要继承 csrf 类完善它里面的功能

# app.py

from flask import Flask, render_template, request
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.csrf.core import CSRF
from wtforms.fields import simple
from hashlib import md5

app = Flask(__name__)


# 继承 CSRF,完善 CSRF 的功能
class MyCSRF(CSRF):
    def setup_form(self, form):
        self.csrf_context = form.meta.csrf_context()
        self.csrf_secret = form.meta.csrf_secret
        return super(MyCSRF, self).setup_form(form)

# 生成 csrf_token(即:随机字符串)
    def generate_csrf_token(self, csrf_token):
        gid = self.csrf_secret + self.csrf_context
        token = md5(gid.encode('utf-8')).hexdigest()
        return token

# 当用户提交数据的时候,验证发送过来的csrf_token是否正确
    def validate_csrf_token(self, form, field):
        print(field.data, field.current_token)
        if field.data != field.current_token:
            raise ValueError('csrf_token 验证失败')


class LoginForm(Form):
    username = simple.StringField(
        label='用户名',
        widget=widgets.TextInput(),
        validators=[
            validators.DataRequired(message='用户名不能为空'),
            validators.Length(min=3, message='用户名长度不能小于%(min)d')
        ],
        render_kw={'placeholder': '请输入用户名'}
    )
    password = simple.PasswordField(
        label='密码',
        validators=[
            validators.DataRequired(message='密码不能为空')
        ],
        widget=widgets.PasswordInput(),
        render_kw={'placeholder': '请输入密码'}
    )

    class Meta:
# ----------- csrf_token -----------
        csrf = True  # 是否自动生成CSRF标签
        csrf_field_name = 'csrf_token'  # 生成CSRF标签name
        csrf_secret = 'xxxxxx'  # 自动生成标签的值,加密用的csrf_secret
        csrf_context = lambda x: request.url  # 自动生成标签的值,加密用的csrf_context
        csrf_class = MyCSRF  # 生成和比较csrf标签所使用到的类


@app.route('/login', methods=['GET', 'POST'])
def login():
    form_obj = LoginForm()
    if request.method == 'POST':
        form_obj = LoginForm(formdata=request.form)  # 一定要使用回 WTForms 进行数据的验证,否则 csrf 验证会无效
        if form_obj.validate():
            return '登陆成功'
        else:
            print(form_obj.errors.get('csrf_token'))  # 获取 csrf_token 的错误信息
    return render_template('login.html', form_obj=form_obj)


if __name__ == '__main__':
    app.run()

# login.html

<form method="post" novalidate>
{{ form_obj.csrf_token }}
    <p>用户名: {{ form_obj.username }}</p>{{ form_obj.username.errors.0 }}
    <p>密码: {{ form_obj.password }}</p>{{ form_obj.password.errors.0 }}
{{ form_obj.csrf_token.errors.0 }}
    <input type="submit" value="提交">
</form>

钩子函数


  • 什么是钩子函数: WTForms源码通过反射找到指定前缀的函数并且执行该函数,即: 该指定前缀的函数就是钩子函数

  • 什么时候使用钩子函数: 当 WTForms 所提供的校验规则无法满足你的校验,那么可以使用钩子函数自定义校验规则

# app.py

from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple


class IndexForm(Form):
    pwd = simple.StringField(
        label='密码',
        widget=widgets.PasswordInput(),
        validators=[
            validators.DataRequired(message='密码不能为空'),
        ]
    )
    pwd_confirm = simple.PasswordField(
        label='重复密码',
        widget=widgets.PasswordInput(),
        validators=[
            validators.DataRequired(message='重复密码不能为空')
        ]
    )

# 定义钩子函数(validate_字段名() 方法),用来校验pwd_confirm字段
    def validate_pwd_confirm(self, field):
"""
        自定义pwd_confirm字段规则,例:与pwd字段是否一致(注意:Flask已经提供了比较两个字段的值是否一致的功能,这里只是为了演示钩子函数的作用)
        参数:
            field.data: pwd_confirm字段所接收到的值
            self.data: 存储着通过检验的字段的数据
        """
        if field.data != self.data['pwd']:
            # raise validators.ValidationError("密码不一致") # 继续当前字段的后续验证(注意:这里的后续验证只的不是下一个字段的验证,而是当前字段的下一个验证,因为一个字段可能会有多个验证)
            raise validators.StopValidation("密码不一致")  # 不再继续当前字段的后续验证(注意:这里的后续验证只的不是下一个字段的验证,而是当前字段的下一个验证,因为一个字段可能会有多个验证)